package none.rg.gantt.model;


import java.util.*;


/**
 * Main class of GanttChart library - represents Chart to which Tasks are added.
 * @author Rodion Gorkovenko
 */
public class Chart {
    
    
    private Map<Integer, Task> tasks = new TreeMap<>();
    private int nextTaskId = 0;
    private int finishTime;
    private boolean recalcEnabled = true;
    private boolean calculable;
    private int linkCount = 0;
    
    
    /**
     * Public constructor.
     */
    public Chart() {
    } // Chart
    
    
    /**
     * Adds "finish-to-start" link between two tasks.
     * @param pred predecessor task
     * @param succ successor task
     * @return link object (needs not to be stored)
     */
    public Link addLink(Task pred, Task succ) {
        return addLink(pred, succ, Link.Type.FINISH_START);
    } // addLink
    
    
    /**
     * Adds link of the given type between two tasks.
     * @param pred predecessor task
     * @param succ successor task
     * @param type the type of link (finish-to-finish, start-to-start etc.)
     * @return link object
     */
    public synchronized Link addLink(Task pred, Task succ, Link.Type type) {
        Link link;
        
        if (succ == null || pred == null) {
            throw new NullPointerException();
        } // if
        
        link = new Link(pred, succ, type);
        
        succ.addPred(link);
        pred.addSucc(link);
        linkCount++;
        
        recalc();
        
        return link;
    } // addLink
    
    
    /**
     * Checks whether two tasks are linked by "finish-to-start" link
     * @param pred predecessor task
     * @param succ successor task
     * @return true if tasks are linked
     */
    public boolean isLinked(Task pred, Task succ) {
        return isLinked(pred, succ, Link.Type.FINISH_START);
    } // isLinked
    
    /**
     * Checks whether two tasks are linked by link of specified type
     * @param pred predecessor task
     * @param succ successor task
     * @param type the type of a link to check
     * @return true if tasks are linked
     */
    public boolean isLinked(Task pred, Task succ, Link.Type type) {
        
        if (succ == null || pred == null) {
            throw new NullPointerException();
        } // if
        
        return succ.getPred(Link.fetchId(pred, type)) != null;
    } // isLinked
    
    
    /**
     * Provides next free task Id, used by tasks when they are created and
     * added to map.
     */
    int fetchNextTaskId() {
        return nextTaskId++;
    } // fetchNextTaskId
    
    
    /**
     * Called by task on creation, so that it could be added to chart.
     * @param task being added
     */
    protected void notifyTaskAdded(Task task) {
        tasks.put(task.getId(), task);
    } // notifyTaskAdded
    
    
    /**
     * Called by task on deletion, so that it could be removed from chart.
     * @param task being deleted
     */
    protected void notifyTaskRemoved(Task task) {
        tasks.remove(task.getId());
    } // notifyTaskRemoved
    
    
    /**
     * Provides unmodifiable view of tasks map
     * @return map of tasks with their ids as keys
     */
    public Map<Integer, Task> getTaskMap() {
        return Collections.unmodifiableMap(tasks);
    } // getTaskMap
    
    
    /**
     * Provides mapping of tasks by their names.
     * Map is constructed each time method is called.
     * @return map of tasks with their names as keys.
     */
    public Map<String, Task> genNamedTaskMap() {
        Map<String, Task> map = new HashMap<>();
        for (Task task : tasks.values()) {
            map.put(task.getTitle(), task);
        } // for
        return map;
    } // genNamedTaskMap
    
    
    /**
     * Allows to enable or disable automatic recalculation of chart
     * (it is performed after each structural change of a chart).
     * It may be necessary to temporarily disable recalculation if
     * lots of changes are performed at once.
     * On enabling recalculation is called automatically.
     * @param b true to enable, false to disable
     */
    public void enableRecalc(boolean b) {
        recalcEnabled = b;
        recalc();
    } // enableRecalc
    
    
    /**
     * Initiates calculation of the chart. If calculation
     * is successful starting and finishing times of all
     * tasks are adjusted and tokens of critical path are added.
     */
    public void calculate() {
        Calculator calc = new Calculator(this);
        
        calculable = calc.process();
        if (!calculable) {
            return;
        } // if
        finishTime = calc.getEndTime();
        
    } // calculate
    
    
    /**
     * After chart calculation this method allows to
     * check if calculation was performed without errors.
     * @return true if chart was calculated correctly.
     */
    public boolean isCalculable() { return calculable; }
    
    /**
     * Returns finish time of the chart (finish time of the
     * latest task).
     * @return time when all tasks are finished
     */
    public int getFinishTime() { return finishTime; }
    
    /**
     * This was intended for counting total link number,
     * however its usefulness is under question and it does
     * not work properly if some links or tasks were removed
     * from chart. (however it allows to check number of links
     * after loading from xml, for example, for testing)
     * @return number of links
     */
    @Deprecated
    public int getLinkCount() { return linkCount; }
    
    
    /**
     * Attempts to perform recalculation of the chart, if it is allowed,
     * see {@link #enableRecalc enableRecalc(boolean)}.
     * Is used internally by methods which change
     * structure of the chart, to update it.
     */
    protected void recalc() {
        
        if (!recalcEnabled) {
            return;
        } // if
        
        calculate();
        
    } // recalc
    
    
    /**
     * Returns list of tasks sorted so that it is convenient
     * to display them (children follows their parent groups and
     * starting times are in ascending order).
     * @return sorted list of tasks
     */
    public List<Task> getTasksSorted() {
        final List<Task> result = new LinkedList<>();
        
        new Object() {
            void traverse(TreeElem elem) {
                List<TreeElem> list = new LinkedList<>(elem.getChildren());
                Collections.sort(list);
                for (TreeElem child : list) {
                    result.add(child.getTask());
                    traverse(child);
                } // for
            }}.traverse(treeView());
        
        return result;
    } // getTasksSorted
    
    
    private TreeElem treeView() {
        TreeElem root = new TreeElem(null);
        Set<TreeElem> groups = new HashSet<>();
        
        for (Task t : tasks.values()) {
            TreeElem elem = new TreeElem(t);
            root.addChild(elem);
            if (t.isOfSpecialType(Group.SPECIAL_TYPE)) {
                groups.add(elem);
            } // if
        } // for
        
        for (TreeElem g : groups) {
            for (Task child : ((Group) g.getTask()).viewChildren()) {
                TreeElem childElem = root.getChild(child.getId());
                root.removeChild(childElem);
                g.addChild(childElem);
            } // for
        } // for
        
        return root;
    } // treeView
    
    
    private static class TreeElem implements Comparable<TreeElem> {
        
        private Task task;
        private TreeElem parent;
        private Map<Integer, TreeElem> children;
        
        protected TreeElem(Task task) {
            children = new HashMap<>();
            this.task = task;
        } // task
        
        public TreeElem getParent() { return parent; }
        protected void setParent(TreeElem e) { parent = e; }
        
        public Task getTask() { return task; }
        
        protected void addChild(TreeElem child) {
            children.put(child.getTask().getId(), child);
            child.setParent(this);
        } // addChild
        
        protected void removeChild(TreeElem child) {
            children.remove(child.getTask().getId());
            child.setParent(null);
        } // removeChild
        
        public TreeElem getChild(int id) {
            return children.get(id);
        } // getChild
        
        protected Collection<TreeElem> getChildren() {
            return children.values();
        } // getChildren
        
        @Override
        public int compareTo(TreeElem other) {
            int firstCmp = Integer.compare(task.getStart(), other.getTask().getStart());
            if (firstCmp != 0) {
                return firstCmp;
            } // if
            return Integer.compare(task.getFinish(), other.getTask().getFinish());
        } // compareTo
        
        @Override
        public boolean equals(Object o) {
            return (o instanceof TreeElem) && ((TreeElem) o).getTask().equals(task);
        } // equals
        
        @Override
        public int hashCode() {
            return task.hashCode();
        } // hashCode
        
    } // TreeElem
    
    
} // class Chart

